using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection.Emit;
using System.Reflection;

// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public License
// as published by the Free Software Foundation; either version 2.1
// of the License, or (at your option) any later version.
// 
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
// 
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free
// Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
// MA 02111-1307, USA.
// 
// Flee# - A port of Eugene Ciloci's Flee to C#
// Copyright © 2012 Yoni Gozman
//

namespace Ciloci.Flee
{
    // Elements that look like normal functions but behave in a special way

	// Implements explicit casts and conversions
	internal class CastElement : ExpressionElement
	{

		private ExpressionElement MyCastExpression;
        // The type we are casting to

		private Type MyDestType;
		public CastElement(ExpressionElement castExpression, string[] destTypeParts, bool isArray, IServiceProvider services)
		{
			MyCastExpression = castExpression;

			MyDestType = GetDestType(destTypeParts, services);

			if (MyDestType == null) {
				base.ThrowCompileException(CompileErrorResourceKeys.CouldNotResolveType, CompileExceptionReason.UndefinedName, GetDestTypeString(destTypeParts, isArray));
			}

			if (isArray == true) {
				MyDestType = MyDestType.MakeArrayType();
			}

			if (this.IsValidCast(MyCastExpression.ResultType, MyDestType) == false) {
				this.ThrowInvalidCastException();
			}
		}

		private static string GetDestTypeString(string[] parts, bool isArray)
		{
			var s = string.Join(".", parts);

			if (isArray == true) {
				s = s + "[]";
			}

			return s;
		}

        // Resolve the type we are casting to
		private static Type GetDestType(string[] destTypeParts, IServiceProvider services)
		{
            var context = (ExpressionContext)services.GetService(typeof(ExpressionContext));

			Type t = null;

			// Try to find a builtin type with the name
			if (destTypeParts.Length == 1) {
				t = ExpressionImports.GetBuiltinType(destTypeParts[0]);
			}

			if ((t != null)) {
				return t;
			}

			// Try to find the type in an import
			t = context.Imports.FindType(destTypeParts);

			if ((t != null)) {
				return t;
			}

			return null;
		}

		private bool IsValidCast(Type sourceType, Type destType)
		{
			if (object.ReferenceEquals(sourceType, destType)) {
				// Identity cast always succeeds
				return true;
			} else if (destType.IsAssignableFrom(sourceType) == true) {
				// Cast is already implicitly valid
				return true;
			} else if (ImplicitConverter.EmitImplicitConvert(sourceType, destType, null) == true) {
				// Cast is already implicitly valid
				return true;
			} else if (IsCastableNumericType(sourceType) & IsCastableNumericType(destType)) {
				// Explicit cast of numeric types always succeeds
				return true;
			} else if (sourceType.IsEnum == true | destType.IsEnum == true) {
				return this.IsValidExplicitEnumCast(sourceType, destType);
			} else if ((this.GetExplictOverloadedOperator(sourceType, destType) != null)) {
				// Overloaded explict cast exists
				return true;
			}

			if (sourceType.IsValueType == true) {
				// If we get here then the cast always fails since we are either casting one value type to another
				// or a value type to an invalid reference type
				return false;
			} else {
				if (destType.IsValueType == true) {
					// Reference type to value type
					// Can only succeed if the reference type is a base of the value type or
					// it is one of the interfaces the value type implements
					Type[] interfaces = destType.GetInterfaces();
					return IsBaseType(destType, sourceType) == true | System.Array.IndexOf(interfaces, sourceType) != -1;
				} else {
					// Reference type to reference type
					return this.IsValidExplicitReferenceCast(sourceType, destType);
				}
			}
		}

		private MethodInfo GetExplictOverloadedOperator(Type sourceType, Type destType)
		{
			var binder = new ExplicitOperatorMethodBinder(destType, sourceType);

			// Look for an operator on the source type and dest types
			var miSource = Utility.GetOverloadedOperator("Explicit", sourceType, binder, sourceType);
			var miDest = Utility.GetOverloadedOperator("Explicit", destType, binder, sourceType);

			if (miSource == null & miDest == null) {
				return null;
			} else if (miSource == null) {
				return miDest;
			} else if (miDest == null) {
				return miSource;
			} else {
				base.ThrowAmbiguousCallException(sourceType, destType, "Explicit");
				return null;
			}
		}

		private bool IsValidExplicitEnumCast(Type sourceType, Type destType)
		{
			sourceType = GetUnderlyingEnumType(sourceType);
			destType = GetUnderlyingEnumType(destType);
			return this.IsValidCast(sourceType, destType);
		}

		private bool IsValidExplicitReferenceCast(Type sourceType, Type destType)
		{
			Debug.Assert(sourceType.IsValueType == false & destType.IsValueType == false, "expecting reference types");

			if (object.ReferenceEquals(sourceType, typeof(object))) {
				// From object to any other reference-type
				return true;
			} else if (sourceType.IsArray == true & destType.IsArray == true) {
				// From an array-type S with an element type SE to an array-type T with an element type TE,
				// provided all of the following are true:

				// S and T have the same number of dimensions
				if (sourceType.GetArrayRank() != destType.GetArrayRank()) {
					return false;
				} else {
					var SE = sourceType.GetElementType();
					var TE = destType.GetElementType();

					// Both SE and TE are reference-types
					if (SE.IsValueType == true | TE.IsValueType == true) {
						return false;
					} else {
						// An explicit reference conversion exists from SE to TE
						return this.IsValidExplicitReferenceCast(SE, TE);
					}
				}
			} else if (sourceType.IsClass == true & destType.IsClass == true) {
				// From any class-type S to any class-type T, provided S is a base class of T
				return IsBaseType(destType, sourceType);
			} else if (sourceType.IsClass == true & destType.IsInterface == true) {
				// From any class-type S to any interface-type T, provided S is not sealed and provided S does not implement T
				return sourceType.IsSealed == false & ImplementsInterface(sourceType, destType) == false;
			} else if (sourceType.IsInterface == true & destType.IsClass == true) {
				// From any interface-type S to any class-type T, provided T is not sealed or provided T implements S.
				return destType.IsSealed == false | ImplementsInterface(destType, sourceType) == true;
			} else if (sourceType.IsInterface == true & destType.IsInterface == true) {
				// From any interface-type S to any interface-type T, provided S is not derived from T
				return ImplementsInterface(sourceType, destType) == false;
			} else {
				Debug.Assert(false, "unknown explicit cast");
			}

            return false;
		}

		private static bool IsBaseType(Type target, Type potentialBase)
		{
			var current = target;
			while ((current != null)) {
				if (object.ReferenceEquals(current, potentialBase)) {
					return true;
				}
				current = current.BaseType;
			}
			return false;
		}

		private static bool ImplementsInterface(Type target, Type interfaceType)
		{
			var interfaces = target.GetInterfaces();
			return Array.IndexOf(interfaces, interfaceType) != -1;
		}

		private void ThrowInvalidCastException()
		{
			base.ThrowCompileException(CompileErrorResourceKeys.CannotConvertType, CompileExceptionReason.InvalidExplicitCast, MyCastExpression.ResultType.Name, MyDestType.Name);
		}

		private static bool IsCastableNumericType(Type t)
		{
			return t.IsPrimitive == true & (!object.ReferenceEquals(t, typeof(bool)));
		}

		private static Type GetUnderlyingEnumType(Type t)
		{
			if (t.IsEnum) {
				return System.Enum.GetUnderlyingType(t);
			} else {
				return t;
			}
		}

		public override void Emit(FleeILGenerator ilg, IServiceProvider services)
		{
			MyCastExpression.Emit(ilg, services);

			var sourceType = MyCastExpression.ResultType;
			var destType = MyDestType;

			this.EmitCast(ilg, sourceType, destType, services);
		}

		private void EmitCast(FleeILGenerator ilg, Type sourceType, Type destType, IServiceProvider services)
		{
			var explicitOperator = this.GetExplictOverloadedOperator(sourceType, destType);

			if (object.ReferenceEquals(sourceType, destType)) {
				// Identity cast; do nothing
				return;
			} else if ((explicitOperator != null)) {
				ilg.Emit(OpCodes.Call, explicitOperator);
			} else if (sourceType.IsEnum == true | destType.IsEnum == true) {
				this.EmitEnumCast(ilg, sourceType, destType, services);
			} else if (ImplicitConverter.EmitImplicitConvert(sourceType, destType, ilg) == true) {
				// Implicit numeric cast; do nothing
				return;
			} else if (IsCastableNumericType(sourceType) & IsCastableNumericType(destType)) {
				// Explicit numeric cast
				EmitExplicitNumericCast(ilg, sourceType, destType, services);
			} else if (sourceType.IsValueType == true) {
				Debug.Assert(destType.IsValueType == false, "expecting reference type");
				ilg.Emit(OpCodes.Box, sourceType);
			} else {
				if (destType.IsValueType == true) {
					// Reference type to value type
					ilg.Emit(OpCodes.Unbox_Any, destType);
				} else {
					// Reference type to reference type
					if (destType.IsAssignableFrom(sourceType) == false) {
						// Only emit cast if it is an explicit cast
						ilg.Emit(OpCodes.Castclass, destType);
					}
				}
			}
		}

		private void EmitEnumCast(FleeILGenerator ilg, Type sourceType, Type destType, IServiceProvider services)
		{
			if (destType.IsValueType == false) {
				ilg.Emit(OpCodes.Box, sourceType);
			} else if (sourceType.IsValueType == false) {
				ilg.Emit(OpCodes.Unbox_Any, destType);
			} else {
				sourceType = GetUnderlyingEnumType(sourceType);
				destType = GetUnderlyingEnumType(destType);
				this.EmitCast(ilg, sourceType, destType, services);
			}
		}

		private static void EmitExplicitNumericCast(FleeILGenerator ilg, Type sourceType, Type destType, IServiceProvider services)
		{
			var desttc = Type.GetTypeCode(destType);
			var sourcetc = Type.GetTypeCode(sourceType);
			bool unsigned = IsUnsignedType(sourceType);
            var options = (ExpressionOptions)services.GetService(typeof(ExpressionOptions));
			bool @checked = options.Checked;
			var op = OpCodes.Nop;

			switch (desttc) {
				case TypeCode.SByte:
					if (unsigned == true & @checked == true) {
						op = OpCodes.Conv_Ovf_I1_Un;
					} else if (@checked == true) {
						op = OpCodes.Conv_Ovf_I1;
					} else {
						op = OpCodes.Conv_I1;
					}
					break;
				case TypeCode.Byte:
					if (unsigned == true & @checked == true) {
						op = OpCodes.Conv_Ovf_U1_Un;
					} else if (@checked == true) {
						op = OpCodes.Conv_Ovf_U1;
					} else {
						op = OpCodes.Conv_U1;
					}
					break;
				case TypeCode.Int16:
					if (unsigned == true & @checked == true) {
						op = OpCodes.Conv_Ovf_I2_Un;
					} else if (@checked == true) {
						op = OpCodes.Conv_Ovf_I2;
					} else {
						op = OpCodes.Conv_I2;
					}
					break;
				case TypeCode.UInt16:
					if (unsigned == true & @checked == true) {
						op = OpCodes.Conv_Ovf_U2_Un;
					} else if (@checked == true) {
						op = OpCodes.Conv_Ovf_U2;
					} else {
						op = OpCodes.Conv_U2;
					}
					break;
				case TypeCode.Int32:
					if (unsigned == true & @checked == true) {
						op = OpCodes.Conv_Ovf_I4_Un;
					} else if (@checked == true) {
						op = OpCodes.Conv_Ovf_I4;
					} else if (sourcetc != TypeCode.UInt32) {
						// Don't need to emit a convert for this case since, to the CLR, it is the same data type
						op = OpCodes.Conv_I4;
					}
					break;
				case TypeCode.UInt32:
					if (unsigned == true & @checked == true) {
						op = OpCodes.Conv_Ovf_U4_Un;
					} else if (@checked == true) {
						op = OpCodes.Conv_Ovf_U4;
					} else if (sourcetc != TypeCode.Int32) {
						op = OpCodes.Conv_U4;
					}
					break;
				case TypeCode.Int64:
					if (unsigned == true & @checked == true) {
						op = OpCodes.Conv_Ovf_I8_Un;
					} else if (@checked == true) {
						op = OpCodes.Conv_Ovf_I8;
					} else if (sourcetc != TypeCode.UInt64) {
						op = OpCodes.Conv_I8;
					}
					break;
				case TypeCode.UInt64:
					if (unsigned == true & @checked == true) {
						op = OpCodes.Conv_Ovf_U8_Un;
					} else if (@checked == true) {
						op = OpCodes.Conv_Ovf_U8;
					} else if (sourcetc != TypeCode.Int64) {
						op = OpCodes.Conv_U8;
					}
					break;
				case TypeCode.Single:
					op = OpCodes.Conv_R4;
					break;
				default:
					Debug.Assert(false, "Unknown cast dest type");
					break;
			}

			if (op.Equals(OpCodes.Nop) == false) {
				ilg.Emit(op);
			}
		}

		private static bool IsUnsignedType(Type t)
		{
			var tc = Type.GetTypeCode(t);
			switch (tc) {
				case TypeCode.Byte:
				case TypeCode.UInt16:
				case TypeCode.UInt32:
				case TypeCode.UInt64:
					return true;
				default:
					return false;
			}
		}

		public override System.Type ResultType {
			get { return MyDestType; }
		}
	}
}
